# Authentification à divulgation nulle de connaissance

## Objectifs

L'objectif de cette activité est de comparer le mécanisme de l'authentification
classique (mot de passe) à celui de la **preuve à divulgation nulle de connaissance**
(cf <abbr title="Zero Knowledge Proof">ZKP</abbr>).

Cette activité aborde aussi le concept d'améliorations progressives (dans
le sens ou l'authentification reste possible *même sans le JavaScript*).

Remarques :

- Cette activité n'utilise aucun cadriciel afin de se focaliser sur l'authentification.
- Le code est presque complet : le principal travail consiste à l'analyser ;
les réponses aux questions de réflexion doivent être rédigées.


## Principes

### Authentification classique (rappel)

Dans un système d'authentification classique, l'utilisateur (via son client)
envoie un secret (mot de passe) au serveur. Le serveur compare ce secret avec
celui stocké (généralement en base de donnéees).

En réalité, **le serveur ne stocke pas le mot de passe** mais son **condensé**
(hash) calculé avec un sel :

- Le **sel** est une chaîne aléatoire ajoutée au mot de passe avant hachage pour
empêcher les attaques par tables arc-en-ciel et garantir un condensé différent
pour des mots de passe identiques ; *le sel est stocké en clair avec le consensé*.
- Une **table arc-en-ciel** (rainbow table) contient des condensés de mot
de passe connus (issus d'un **dictionnaire**) précalculés permettant de retrouver
rapidement un mot de passe à partir de son condensé.
- **Un mot de passe ne peut pas être retrouvé à partir de son condensé** :
pour vérifier la correspondance, il faut calculer le condensé du mot de passe
saisit par l'utilisateur (en utilisant le même sel que celui stocké).

Ce mécanisme d'authentification présente des inconvénients :

- Si un attaquant parvient à récupérer les condensés des mots de passe, il
peut réaliser une attaque par force brute pour les casser.
- Une attaque par **force brute**, qui consiste a essayer toutes les combinaisons
possibles de caractères, est d'autant plus coûteuse que les mots de passe
sont longs et constitués de **classes** différentes **de caractères** (majuscules,
minuscules, chiffres, caractères spéciaux) ; en pratique, ce type d'attaque s'appuie généralement
sur un dictionnaire comme [RockYou](https://github.com/dw0rsec/rockyou.txt),
combiné à des substitutions (a → @, e → €, s → 5…).
- Le **mot de passe** peut être **intercepté** (d'où l'importance du protocole
<abbr title="HyperText Transfer Protocol Secure">HTTPS</abbr>).

Des algotithmes comme Argon2ID, BCrypt ou YesCrypt rendent difficiles les
attaques par force brute car ils sont intentionnellement coûteux en mémoire
et en temps de calcul, y compris sur <abbr title="Graphical Processing Unit">GPU</abbr>.

Il est recommandé pour les utilisateurs d'utiliser un **gestionnaire de mots
de passe** (celui intégré au navigateur, BitWarden, KeePass…) et de définir
un **mot de passe long, complexe, aléatoire et différent pour chaque service**.


### Authentification Défi-Réponse (ZKP)

Dans une approche Défi-Reponse, l'authentification se fait à divulgation nulle
de connaissance en s'appuyant sur la cryptographie à clés publiques.

- Le serveur génère un défi (chaîne aléatoire) et l'envoie au client.
- Le client signe ce défi avec la clé privée de l'utilisateur et renvoie
la signature ; *la clé privée n'est pas transmise au serveur — cf ZKP*.
- Le serveur vérifie la signature avec la clé publique qu'il possède.

![Diagramme des flux](Schéma authentification.svg "Schéma du processus d'authentification")

Les inconvénients de l'authentification classiques sont ainsi résolus :

- Le serveur ne stocke pas de secret (ni de condensé), et il n'est pas possible
de retrouver une clé privée à partir d'une clé publique.
- Aucun secret ne circule sur le réseau.

Attention, il est important que le client ne puisse pas choisir le défi, car
cela permettrait à un attaquant de rejouer une ancienne requête d'authentification
qu"il serait parvenu à capturer.

### Compléments techniques

- Lors de l'envoi du formulaire d'authentification, le serveur doit mémoriser
le défi généré, par exemple en session. Sa réponse comprend alors un cookie
contenant l'identifiant de session.


- Si l'authentification réussit, le serveur doit mémoriser l'état :
	- dans la session
	- ou, sans session, en générant un jeton (cf <abbr title="JSON Web Token">JWT</abbr>)
	qu'il envoie (cookie) au client.

- Généralement, la session expire après une certaine durée d'inactivité.


## Préparation

Sur l'hébergement web :

- créer un dossier `auth` ;
- créer un sous-dossier `db` (dans `auth`) ;
- placer le script `init-auth-db.sh` dans le dossier `db` et le rendre exécutable
(`chmod +x init-auth-db.sh`) ;
- exécuter le script `./init-auth-db.sh` depuis le dossier `db` pour créer
deux utilisateurs, un avec mot de passe et l'autre sans (noter le secret !) ;
- vérifier le résultat :
	- `ls -l` : le fichier `db.sqlite` doit avoir le droit d'écriture pour tous ;
	- `echo "SELECT * FROM Auth" | sqlite3 db.sqlite` doit afficher l' utilisateur.

### Script init-auth-db.sh

Ce script suivant permet de mettre en place une table dédiée à l'authentification
des utilisateurs et de les inscrire. Quelques explications :

- Le champ `key` sert à enregistrer la clé publique — ou le condensé du mot
de passe dans le cas d'une authentification classique.
- Le champ `totp` sert à générer le code à usage unique (<abbr title="Time-based One-Time Password">TOTP</abbr>)
pour une authentification forte (avec second facteur) — non utilisé dans ici.
- Le champ `recovery` sert à la récupération du compte en cas de perte de
clé privée (ou mot de passe oublié) ; il doit alors contenur un secret (envoyé
par courriel) et une date limite d'utilisation (de l'ordre de quelques minutes).


```sh
#!/bin/bash
set -euo pipefail

readonly TOTP_ISSUER="web.sio.local"


if [ ! -f db.sqlite ]; then
	echo "CREATE TABLE Auth (id INTEGER PRIMARY KEY AUTOINCREMENT, "\
		 "email VARCHAR(32) NOT NULL UNIQUE, key VARCHAR(64),"\
		 "totp VARCHAR(32), recovery VARCHAR(32), locking INT DEFAULT 0);" | sqlite3 db.sqlite
	#chmod g+rw . db.sqlite
	chmod 707 . ; chmod 606 db.sqlite
fi


echo -n "Email: "; read email
if [ -z "$email" ]; then exit; fi
echo -n "Password (leave empty for ZKA): "; read password
echo -n "Use TOTP [y/N]: "; read use_totp


if [ -z "$password" ]; then
	priv=$(openssl genpkey -algorithm ED25519) #éviter les fichiers intermédiaires
	pub=$(echo -e "$priv" | openssl pkey -pubout)
	key=$(echo -e "$pub" | sed '1d;$d') #| tr -d'\n'
	secret=$(echo -e "$priv" | sed '1d;$d') #| tr -d'\n'
else
	salt=$(openssl rand 16)					#chaîne binaire
	#saltHex=$(echo "$salt" | od -An -tx1)	#→ hexadécimal
	key=$(echo -n "$password" | argon2 "$salt" -id -e) #ne pas oublier "-n" !
	secret=$password
fi

echo "INSERT INTO Auth (email, key) VALUES ('$email', '$key');" | sqlite3 db.sqlite

if [ "$use_totp" == "y" ] || [ "$use_totp" == "Y" ]; then
	totp=$(openssl rand 20 | base32)
	echo "UPDATE Auth SET totp='$totp' WHERE email = '$email'" | sqlite3 db.sqlite
fi


echo -e "\nEmail: $email"
echo "Secret: $secret"
if [ "$use_totp" == "y" ] || [ "$use_totp" == "Y" ]; then
	uri="otpauth://totp/$TOTP_ISSUER:$email?secret=$totp&issuer=$TOTP_ISSUER"
	echo "TOTP: $(echo "$totp" | sed -E -e 's/([A-Z2-7]{4})/\1 /g' -e 's/ $//')"
	qrencode -t ANSIUTF8 "$uri"
fi
```

### Questions de réflexion

Analyser le script et expliquer :

- `set -euo pipefail` ;
- `if [ ! -f db.sqlite ]; then` ;
- la contrainte `UNIQUE` sur le champ `email` ;
- `chmod a+w . db.sqlite` ;
- repérer les commandes qui génèrent :
	- la clé privée,
	- la clé publique,
	- le condensé (hash) du mot de passe ;
- le sel — encodé en base64 — est présent (en clair) dans le condensé ; vérifier
qu'il correspond (cf ligne commentée `saltHex=…`) ;
- indiquer (avec justifications à l'appui) si le serveur stocke des secrets,
et, le cas échéant, expliquer comment ils pourraient être exploités.


## Mise en place de l'authentification

### Arborescence de l'application

Organisation du dossier `auth` :

	├── auth.js
	├── auth.php
	├── check-auth.php
	├── db
	│   ├── .htaccess
	│   ├── db.sqlite
	│   └── init-auth-db.sh
	├── index.php
	└── logout.php


### Fichier index.php

Ce fichier — qui sert de point d'entrée — contient un contenu protégé (inaccessible
si l'utilisateur n'est pas authentifié) ; il s'appuie sur `check-auth.php` 
pour vérifier si l'utilisateur est authentifié et le rediriger vers `auth.php`
si ce n'est pas le cas.

```php
<?php require "check-auth.php"; ?>
<!DOCTYPE html>
<html lang="fr"><head>
	<meta charset="UTF-8"/>
	<title>Welcome</title>
	<link rel="stylesheet" href="styles.css" integrity="sha256-oMAvPtG9s6sdeBZJdAy9tHWT10hViKkooHuBneJkCrw="/>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head><body>
	<h1>Welcome</h1>
	<p>Welcome <?=$email?>! This content is protected.</p>
	<p><a href="logout.php">Logout</a></p>
</body></html>
```

### Fichier check-auth.php

```php
<?php
session_start();
if (!isset($_SESSION['email'])) {
	header('location: auth.php?reason=' . urlencode('Authentication required'));
	exit;
}
$now = time();
if ($now > $_SESSION['ts']) {
	header('location: auth.php?reason=' . urlencode('Session expired'));
	exit;
}	
$_SESSION['ts'] = $now + INACTIVITY_TIMEOUT; //prolonge la session;
?>
```

### Fichier auth.php

Ce fichier gère l'authentification avec signature (ZKP), une clé privée ou
un mot de passe. Il peut être appelé :

- avec la méthode `GET` pour obtenir le formulaire d'authentification ;
- avec la méthode `POST` pour soumettre les codes d'accès ;


```php
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
	$challenge = bin2hex(random_bytes(32));
	$_SESSION['challenge'] = $challenge;
?>
<!DOCTYPE html>
<html lang="fr"><head>
	<meta charset="UTF-8"/>
	<title>Authentication</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
	<style>h1 { text-align: center; }
label { font-weight: bold;}
input { box-sizing: border-box; width: 100%; border: 1px solid #ccc; padding: 0.25em;}
button { float:right; padding: 0.25em 1em; }
label, input, button  { display: block; }
label, button { margin-top: 1em; }
p.error { color: red; }
	</style><!--FIXME: CSP-->
	<script src="auth.js" defer="defer"></script>
</head><body>
	<h1>Authentication</h1>
	<form id="auth" method="POST">
		<input type="hidden" id="challenge" value="<?=$challenge?>"/>
		<label for="email">Email *:</label>
			<input type="email" id="email" name="email" required="required"/>
		<label for="password">Secret *:</label>
			<input type="password" id="password" name="password" required="required"/>
		<button type="submit">Login</button>
		<p><a href="register.php">Register</a>&nbsp;
		<?php if (isset($_GET['reason'])) { ?>
			<span class="error"><?=htmlspecialchars($_GET['reason'], ENT_QUOTES, 'UTF-8')?></span>
		<?php } ?>
		</p>
	</form>
</body></html>
<?php
} else {
	//convertit hexa → str, et garantit une longueur de 64 octets.
	function loadHexSignature(string $hexSig): string {
		$padding = str_repeat('0', SODIUM_CRYPTO_SIGN_BYTES /*64*/);
		if (strlen($hexSig) % 2 == 0 && preg_match('/^[0-9a-fA-F]*$/', $hexSig)) {
			$sig = substr($padding . hex2bin($hexSig), -SODIUM_CRYPTO_SIGN_BYTES);
		} else {
			$sig = $padding;
		}
		return $sig;
	}

	//décode le base64, extrait la clé et garantit une longueur de 32 octets
	function loadRawKey(string $b64Key): string {
		$rawKey = substr(base64_decode($b64Key), -SODIUM_CRYPTO_SIGN_SEEDBYTES); //mode non strict
		if (strlen($rawKey) < SODIUM_CRYPTO_SIGN_SEEDBYTES /*32*/) {
			$rawKey .= str_repeat('0', SODIUM_CRYPTO_SIGN_SEEDBYTES - strlen($rawKey));
		}
		return $rawKey;
	}
		
	$pdo = new PDO('sqlite:db/db.sqlite'); //FIXME: utiliser un fichier de configuration
	if (!isset($_POST['email']) || '' === $_POST['email'] ||
		!isset($_POST['password']) || '' === $_POST['password']) {
		header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
		die('error: missing email or password');
	}
	$stmt = $pdo->prepare('SELECT key FROM Auth WHERE email = ?');
	$stmt->execute([$_POST['email']]);
	$key = $stmt->fetchColumn();
	//le champ key de la table peut être :
	$pubRaw = loadRawKey($row['key']); //la clé publique!
	$hash = $row['key']; //le condensé du mot de passe
	
	//le champ password du formulaire peut être :
	$sig = loadHexSignature($password); //la signature (ZKP)
	$privRaw = sodium_crypto_sign_seed_keypair(loadRawKey($password)); //la clé privée (JS désactivé)
	
	$chlg = $_SESSION['challenge'] ?? bin2hex(random_bytes(32)); //si le challenge n'est pas en session, échec assuré
	
	$success = sodium_crypto_sign_verify_detached($sig, $chlg, $pubRaw) || //soit la signature est valide
			   $pubRaw === sodium_crypto_sign_publickey($privRaw) || //soit la clé privée correspond à la publique
			   password_verify($password, $hash); //soit le mot de passe correspond au condensé
	
	if ($success) {
		session_regenerate_id(true);
		$_SESSION['email'] = $_POST['email'];
		$_SESSION['ts'] = time() + 60; //1 minute
		header('location: index.php');
	} else {
		header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized');
		header('location: auth.php?reason=' . urlencode('Codes d\'accès incorrects'));
	}
}
?>
```

Remarque ; ce code contient une erreur à corriger : il ne gère pas le cas où
l'email saisit n'existe pas.

### Fichier auth.js

Ce fichier sert à intercepter la soumission du formulaire en remplaçant la
clé privée saisie (à priori depuis un gestionnaire de mots de passe) par la
signature du défi.

```js
"use strict";

document.addEventListener("DOMContentLoaded", () => {
	document.getElementById("auth").onsubmit = async e => {
		e.preventDefault();
		try {
			const secretInput = document.getElementById("password");
			//une exception sera levée si la saisie ne correspond pas à une clé privée / est un mot de passe
			const secret = Uint8Array.fromBase64(secretInput.value); //base64 → bytes
			const key = await crypto.subtle.importKey("pkcs8", secret, { name: "Ed25519" }, false, ["sign"]);
			
			const challengeInput = document.getElementById("challenge");
			const challenge = new TextEncoder().encode(challengeInput.value); //hexa → bytes
			
			const signature = await crypto.subtle.sign("Ed25519", key, challenge);	
			secretInput.value = new Uint8Array(signature).toHex(); //bytes → hexa
		} catch (e) {}
		e.target.submit();
	};
});
```

Remarque : pour uniformiser la déclaration des fonctions événementielles de
rappels, réécrire `document.getElementById("auth").onsubmit = async e => { … }`
en utilisant la méthode `addEventListener` (à la place de `onsubmit`).

### Autres fichiers

- Compléter le fichier `logout.php` qui permet de se déconnecter (en détruisant la
session) et de rediriger l'utilisateur vers la page d'authentification.
- Compléter le fichier `.htaccess` pour empêcher le téléchargement de la base
de données.

### Questions de réflexion

*Ces points doivent être abodés en tenant compte des problématiques de sécurité,
mais aussi de qualité.*

- Code PHP 1 :
	- Expliquer le rôle de la fonction `urlencode`.
	- Expliquer le bloc `if ($now > $_SESSION['ts']) { … }` (fichier `check-auth.php`)
	et comment est gérée le maintien de la connexion en cas d'activité.
- Code <abbr title="HyperText Markup Language">HTML</abbr> :
	- Expliquer ce qu'est la <abbr title="Content Security Policy">CSP</abbr>
	et le problème avec le code HTML.
	- Expliquer l'attribut `defer="defer"` d'une balise `&lt;script>`.
	- Rappler le rôle des attributs `for` (accessibilité), `id` et `name`.
- Code Javascript :
	- Expliquer le rôle de la directive `"use strict"`
	- Expliquer `document.addEventListener("DOMContentLoaded", fonctionDeRappel)`.
	- Expliquer `e.preventDefault()` et `e.target.submit()`.
	- Expliquer le rôle du champ caché `challenge`.
	- Déboguer (pas à pas) la fonction qui calcule la signature.
	- Expliquer ce qui se passe lorsque le JavaScript lève une exception.
- Code PHP 2 :	
	- Analyser comment le PHP gère les différents types de contenu pour le
	champ `key` de la base de données, et les différents types de valeurs
	pour le champ `password` du formulaire.
	- Expliquer l'instruction `session_regenerate_id()`.
	
**Expliquer pourquoi la signature permet de prouver l'identité de l'utilisateur**
(cf cryptographie à clés publiques).


## Activités complémentaires

### L'enregistrement des nouveaux utilisateurs

Ajouter une page permettant aux utilisateurs de s'enregistrer :

- soit en définissant un mot de passe,
- soit en laissant le serveur leur générer une paire de clés,
- ou encore en soumettant une clé publique au serveur.

### Limiter les tentatives

Adapter le code pour limiter les tentatives de connexion (verrouillage d'un
compte pendant 5' au troisième essai infructueux - cf champ `locking` de la table).

### Sécurité en profondeur

Améliorer la sécurité de l'application :

- Correction de la faille <abbr title="cross(X) Site Scripting">XSS</abbr>.
- Forcer le passage en HTTPS si la requête est en HTTP.
- Au niveau des entêtes HTTP, se documenter et :
	- durcir la politique des cookies (`secure`, `httponly`) ;
	- forcer le <abbr title="HTTP Strict Transport Security">HSTS</abbr> ;
	- durcir la politique CSP : seul le contenu en provenance du même serveur
	est autorisé (cf `self`) et le contenu intégré au HTML (<abbr title="Cascading Style Sheets">CSS</abbr> et
	Javascript notamment) doit être rigouresement interdit.
- Ajout des <abbr title="SubRessource Integrity">SRI</abbr> pour les fichiers
Javascript et le CSS (cf utilitaire `sha256sum`).

### Journalisation des accès

Enregistrer dans un fichier de journal (log) les tentatives de connexion (adresse
de courriel, date / heure, adresse <abbr title="Internet Protocol">IP</abbr>, réussite / échec).
*Ce fichier journal doit être protégé (empêcher son téléchargement).*


## Conclusion

Prouver son identité sans divulguer de secret suit le même processus que la
signature de documents :

- le serveur envoie un document au client (le défi pour l'authentification) ;
- le client signe le document (avec la clé privée) et renvoie le document signé ;
- le serveur vérifie la signature.

Remarque : la solution développée dans cette activitée est hybride :

- preuve à divulgation nulle de connaissance ;
- comparaison de la clé privée à la clé publique - ce qui n'est sans doute
pas une bonne idée, car la clé privée est transmise au serveur (utilile pour
l'amélioration progressive) ;
- authentification classique (mot de passe, sel, condensé).
